package detector.audio;

import java.net.URI;
import java.util.Collection;

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultHttpClient;

import detector.audio.processing.*;
import edu.washington.cs.sounddetector.server.*;
import edu.washington.cs.sounddetector.server.api.SoundDetectorAPI;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import edu.washington.cs.sounddetector.server.api.APIConsts;
import edu.washington.cs.sounddetector.server.api.SoundDetectorAPI;
import edu.washington.cs.sounddetector.server.api.exception.CantBuildClassifierException;
import edu.washington.cs.sounddetector.server.api.exception.DetectorDoesNotExistException;
import edu.washington.cs.sounddetector.server.api.exception.SoundClassDoesNotExistException;
import edu.washington.cs.sounddetector.server.api.exception.SoundDetectorInternalErrorException;
import edu.washington.cs.sounddetector.server.api.object.LabeledSound;
import edu.washington.cs.sounddetector.server.api.object.SoundFormat;

/**
 * Handles communication with server. Use this to do any communication.
 * 
 * @author Kyle
 * 
 */
public class ServerComm {
	// TODO: This has some design problems mainly with how it is used. It would
	// be better to have SoundClass handle getting stuff from the server,
	// instead of having the activity call the update method. Abstractly, we
	// just want the list of sound classes that is in sync with the server,
	// and we don't want to have to worry about synchronizing it manually or
	// anything, we want the SoundClass class to take care of that.

	// TODO: Add in call to setColor
	public static final String SERVER_ADDRESS = "http://kiska.cs.washington.edu:8080/sound-detector";
	public static final String DEFAULT_LOCATION_NAME = "LocationLoadingError";
	public static String locationName; // Location of this phone in the house,
										// for example "Living Room"

	private static SoundDetectorAPI api;
	private static String TAG = "ServerComm";

	// Checks if it has been initialized and
	// does so if it hasn't

	private static void checkInit() throws URISyntaxException {
		if (api == null) {
			HttpClient client = new DefaultHttpClient();
			URI serverURI = new URI(SERVER_ADDRESS);
			api = new SoundDetectorAPI(serverURI, client);
		}
	}

	/**
	 * Adds the examples as training data for the classifier for
	 * ServerComm.locationName. Uses DEFAULT_LOCATION_NAME if locationName not
	 * set. You may want to use addToServer if you are doing UI stuff with this,
	 * it does the popup and waiting stuff for you.
	 * 
	 * @effects If successful, adds the exampleIds returned by the server to sc
	 * 
	 * @param sc
	 *            soundclass to add examples for
	 * @param examples
	 *            the examples to add
	 * @throws URISyntaxException
	 * @throws SoundDetectorInternalErrorException
	 * @throws IOException
	 * @throws CantBuildClassifierException
	 * @throws ClientProtocolException
	 */
	public static void sendTrainingExamples(SoundClass sc,
			Collection<AudioClip> examples) throws URISyntaxException,
			ClientProtocolException, CantBuildClassifierException, IOException,
			SoundDetectorInternalErrorException {
		checkInit();

		List<LabeledSound> toSend = new ArrayList<LabeledSound>();

		for (AudioClip ac : examples) {
			LabeledSound ls = new LabeledSound(sc.getName(), ac.getBytes());
			toSend.add(ls);
		}

		// Add to server and associate sound class with these examples.
		sc.addExampleIds(api.train(locationName, toSend,
				AudioClip.getSoundFormat()));
	}

	/**
	 * Removes this sound and examples from the server
	 * 
	 * @param sc
	 *            SoundClass to remove
	 * @throws SoundDetectorInternalErrorException
	 * @throws IOException
	 * @throws CantBuildClassifierException
	 * @throws ClientProtocolException
	 */
	public static void deleteSound(SoundClass sc)
			throws ClientProtocolException, CantBuildClassifierException,
			IOException, SoundDetectorInternalErrorException {
		api.untrain(sc.getExampleIds());
	}

	/**
	 * Sends a detected audio clip to server for classification and alerting for
	 * ServerComm.locationName. Uses DEFAULT_LOCATION_NAME if locationName not
	 * set.
	 * 
	 * @param ac
	 *            audio clip detected
	 * @throws SoundDetectorInternalErrorException
	 * @throws IOException
	 * @throws ClientProtocolException
	 * @throws URISyntaxException
	 * @return the classification result
	 */
	public static SoundClass sendDetectedPeak(AudioClip ac)
			throws ClientProtocolException, IOException,
			SoundDetectorInternalErrorException, URISyntaxException {
		checkInit();
		
		//TODO: Make this thread safe, interleaving calls with changing color can break it

		String result = api.classify(locationName, ac.getBytes(),
				AudioClip.getSoundFormat(), ac.getPeakEvents());

		Log.i(TAG, "Classified a sound as " + result);
		return SoundClass.getSoundClass(result);
	}

	/**
	 * Sets the location name to use for this phone when communicating with the
	 * server. Moves all the soundclasses to that locationName.
	 * 
	 * @param locationName
	 * @throws SoundDetectorInternalErrorException
	 * @throws IOException
	 * @throws DetectorDoesNotExistException
	 * @throws ClientProtocolException
	 */
	public static void setLocationName(String locationName) {
		// TODO: Be consistent about throwing exceptions - this method is
		// inconsistent
		// with what the API does in other places. Also, ServerComm is becoming
		// a god class.
		// figure out how this can be avoided
		try {
			checkInit();
		} catch (URISyntaxException e) {
			Log.e(TAG, Log.getStackTraceString(e));
		}
		// TODO: We don't need to update the server every time on startup, but
		// this is safe, just unnecessary
		if (ServerComm.locationName == null) {
			ServerComm.locationName = locationName;
		} else if (!ServerComm.locationName.equals(locationName)) {
			try {
				api.renameDetector(ServerComm.locationName, locationName);
			} catch (Exception ex) {
				Log.e(TAG, Log.getStackTraceString(ex));
			}
			ServerComm.locationName = locationName;
		}
	}

	/**
	 * Get the location name, null if not set
	 * 
	 * @return
	 */
	public static String getLocationName() {
		return ServerComm.locationName;
	}

	/**
	 * Get the sound classes stored on this server under this phone's name
	 * (specified by getLocationName). Update the SoundClasses on the phone
	 * using that information so that the server and phone match.
	 * 
	 * @throws URISyntaxException
	 * @throws SoundDetectorInternalErrorException
	 * @throws IOException
	 * @throws DetectorDoesNotExistException
	 * @throws ClientProtocolException
	 */
	public static void updateSoundClassesFromServer()
			throws URISyntaxException, ClientProtocolException, IOException,
			SoundDetectorInternalErrorException {
		checkInit();

		// Get the map of names to exampleIds from the api
		Map<edu.washington.cs.sounddetector.server.api.object.SoundClass, List<Long>> sounds;
		try {
			sounds = api.getSounds(locationName);
		} catch (DetectorDoesNotExistException e) {
			// The detector with that name doesn't exist, so just
			// return without doing anything, but log it
			Log.w(TAG, Log.getStackTraceString(e));
			return;
		}
		// Clear all the sound classes
		SoundClass.clear();
		for (edu.washington.cs.sounddetector.server.api.object.SoundClass soundClass : sounds
				.keySet()) {
			// Don't add the class representing uknown to the list
			// Add the soundclass if it doesn't already exist on the phone
			SoundClass sc = SoundClass
					.getSoundClass(soundClass.getSoundClass());
			sc.setColor(soundClass.getRgbColor());
			// Add all the examples ids the server has for this soundclass
			// to
			// the
			// soundclass stored on the phone
			sc.addExampleIds(sounds.get(soundClass));
		}

	}

	/**
	 * Set the color of the soundClass stored on the server
	 * 
	 * @param soundClass
	 * @throws SoundDetectorInternalErrorException
	 * @throws IOException
	 * @throws SoundClassDoesNotExistException
	 * @throws ClientProtocolException
	 * @throws DetectorDoesNotExistException
	 */
	public static void setColor(SoundClass soundClass)
			throws DetectorDoesNotExistException, ClientProtocolException,
			SoundClassDoesNotExistException, IOException,
			SoundDetectorInternalErrorException {
		List<edu.washington.cs.sounddetector.server.api.object.SoundClass> res = new ArrayList<edu.washington.cs.sounddetector.server.api.object.SoundClass>();
		edu.washington.cs.sounddetector.server.api.object.SoundClass apiSc = new edu.washington.cs.sounddetector.server.api.object.SoundClass();
		apiSc.setSoundClass(soundClass.getName());
		apiSc.setRgbColor(soundClass.getColor());
		res.add(apiSc);
		api.setSoundClassProperties(locationName, res);
	}

}
